package com.maccman.activeresource
{
	import mx.collections.ArrayCollection;
	import mx.utils.ObjectProxy;
	import mx.events.PropertyChangeEvent;
	import mx.collections.IViewCursor;
	import mx.events.CollectionEvent;
	import mx.events.CollectionEventKind;

	public class IndexedCollection extends ArrayCollection
	{
		private var _primaryKey:String;
		private var indices:Object = {};
		
		public function IndexedCollection(source:Array = null, primaryKey:String = null)
		{
			super(source);
			addEventListener(CollectionEvent.COLLECTION_CHANGE,
				function(e:CollectionEvent):void {
					if (e.kind == CollectionEventKind.RESET) {
						updateIndex();
					}
				});
			if (primaryKey) {
				this.primaryKey = primaryKey;
			}
		}
		
		private function updateIndex(attributeName:String = null):void {
			clearIndex(attributeName);
			var cursor:IViewCursor = createCursor();
			while (!cursor.afterLast) {
				indexItem(cursor.current, attributeName);
				cursor.moveNext();
			}
		}
		
		private function clearIndex(attributeName:String = null):void {
			if (attributeName) {
				indices[attributeName].hashMap = {};
			} else {
				for (var i:String in indices) {
					clearIndex(i);
				}
			}
		}
		
		private function indexItem(item:Object, attributeName:String = null):void {
			if (attributeName) {
				var val:String = item[attributeName].toString();
				if (indices[attributeName].unique && indices[attributeName].hashMap.hasOwnProperty(val) &&
					indices[attributeName].hashMap[val].length > 0) {
					throw new Error('Duplicate key violation for attribute ' + attributeName + ' with value ' + val.toString());
				}				
				if (indices[attributeName].hashMap[val] == null) {
					indices[attributeName].hashMap[val] = [];
				}
				indices[attributeName].hashMap[val].push(item);
			} else {
				for (var i:String in indices) {
					indexItem(item, i);
				}
			}
		}
		
		public function addIndex(attributeName:String, unique:Boolean = false):void {
			if (hasIndex(attributeName)) {
				throw new Error('Cannot create duplicate index for attribute ' + attributeName);
			} else {
				indices[attributeName] = { unique: unique, hashMap: {} };
			}
			updateIndex(attributeName);
		}
		
		public function hasIndex(attributeName:String):Boolean {
			return indices.hasOwnProperty(attributeName);
		}
		
		public function hasUniqueIndex(attributeName:String):Boolean {
			if (hasIndex(attributeName)) {
				return indices[attributeName].unique;
			} else {
				return false;
			}
		}
		
		public function get primaryKey():String {
			return _primaryKey;
		}
		
		public function set primaryKey(attributeName:String):void {
			if (!hasUniqueIndex(attributeName)) {
				if (hasIndex(attributeName)) {
					throw new Error('Cannot set primary key to ' +
						attributeName + ' because a non-unique index already ' +
						'exists for that attribute');
				} else {
					addIndex(attributeName, true);
				}
			}
			_primaryKey = attributeName;
		}
		
		public override function addItemAt(item:Object, index:int):void {
			if (primaryKey && !item.hasOwnProperty(primaryKey)) {
				throw new Error('Attempted to add an object to the ' +
					'collection which does not have an attribute matching ' +
					'the primary key for the collection.');
			}
			indexItem(item);
			item.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE,
				function(e:PropertyChangeEvent):void {
					if (hasIndex(e.property.toString())) {
						updateItemIndex(e.property.toString(), item, e.oldValue.toString(), e.newValue.toString());
					}
				});
			super.addItemAt(item, index);
		}
		
		private function updateItemIndex(attr:String, item:Object, oldVal:String, newVal:String):void {
			var items:Array = indices[attr].hashMap[oldVal];
			for (var i:int = 0; i < items.length; i++) {
				if (items[i] === item) {
					items.splice(i,1);
				}
			}
			if (indices[attr].hashMap[newVal] == null) {
				indices[attr].hashMap[newVal] = [];
			}
			indices[attr].hashMap[newVal].push(item);
		}
		
		public function findByIndex(attributeName:String, attributeValue:Object):Array {
			if (indices[attributeName].hashMap.hasOwnProperty(attributeValue.toString())) {
				return indices[attributeName].hashMap[attributeValue];
			} else {
				return [];
			}
		}
		
		public function findFirstByIndex(attributeName:String, attributeValue:Object):Object {
			return findByIndex(attributeName, attributeValue)[0];
		}
		
		public function find(attributeValue:Object):Object {
			return findFirstByIndex(primaryKey, attributeValue);
		}
		
		public override function removeItemAt(index:int):Object {
			var item:Object = super.removeItemAt(index);
			for (var i:String in indices) {
				var ival:String = item[i];
				var items:Array = indices[i].hashMap[ival];
				for (var j:int = 0; j < items.length; j++) {
					if (items[j] === item) {
						indices[i].hashMap[ival].splice(j,1);
					}
				}
			}
			return item;
		}
		
		public override function removeAll():void {
			super.removeAll();
			clearIndex();
		}
	}
}